﻿

using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Text;
using System;
using System.IO;
using System.IO.Compression;

namespace Mobile.Utility.Lib.IO.Compression
{
    public class ZipLibraryEntry
    {
        public DateTimeOffset LastWriteTime
        {
            get
            {
                return _lastModified;
            }
            set
            {
                ThrowIfInvalidLibrary();
                if (_Library.Mode == ZipLibraryMode.Read)
                    throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.ReadOnlyLibrary);
                if (_Library.Mode == ZipLibraryMode.Create && _everOpenedForWrite)
                    throw new IOException(Mobile.Utility.Lib.IO.Compression.Messages.FrozenAfterWrite);
                if (!ZipHelper.IsConvertibleToDosTime(value.DateTime))
                    throw new ArgumentOutOfRangeException("value", Mobile.Utility.Lib.IO.Compression.Messages.DateTimeOutOfRange);

                _lastModified = value;
            }
        }
        /// <summary>
        /// The relative path of the entry as stored in the Zip Library. Note that Zip Librarys allow any string to be the path of the entry, including invalid and absolute paths.
        /// </summary>
        public String FullName
        {
            get
            {
                Contract.Ensures(Contract.Result<String>() != null);
                return _storedFilename;
            }

            private set
            {
                if (ZipHelper.RequiresUnicode(value))
                {
                    _generalPurposeBitFlag |= BitFlagValues.UnicodeFileName;
                }
                else
                {
                    _generalPurposeBitFlag &= ~BitFlagValues.UnicodeFileName;
                }

                if (ZipHelper.EndsWithDirChar(value))
                {
                    VersionToExtractAtLeast(ZipVersionNeededValues.ExplicitDirectory);
                }

                _storedFilename = value;
                _storedFilenameBytes = FilenameEncoding.GetBytes(_storedFilename);
            }
        }
        /// <summary>
        /// The filename of the entry. This is equivalent to the substring of Fullname that follows the final directory separator character.
        /// </summary>
        public String Name
        {
            get
            {
                Contract.Ensures(Contract.Result<String>() != null);
                return Path.GetFileName(FullName);
            }
        }
        /// <summary>
        /// The compressed size of the entry. If the Library that the entry belongs to is in Create mode, attempts to get this property will always throw an exception. If the Library that the entry belongs to is in update mode, this property will only be valid if the entry has not been opened. 
        /// </summary>
        /// <exception cref="InvalidOperationException">This property is not available because the entry has been written to or modified.</exception>
        public Int64 CompressedLength
        {
            get
            {
                Contract.Ensures(Contract.Result<Int64>() >= 0);

                if (_everOpenedForWrite)
                    throw new InvalidOperationException(Mobile.Utility.Lib.IO.Compression.Messages.LengthAfterWrite);
                return _compressedSize;
            }
        }
        /// <summary>
        /// The uncompressed size of the entry. This property is not valid in Create mode, and it is only valid in Update mode if the entry has not been opened.
        /// </summary>
        /// <exception cref="InvalidOperationException">This property is not available because the entry has been written to or modified.</exception>
        public Int64 Length
        {
            get
            {
                Contract.Ensures(Contract.Result<Int64>() >= 0);

                if (_everOpenedForWrite)
                    throw new InvalidOperationException(Mobile.Utility.Lib.IO.Compression.Messages.LengthAfterWrite);
                return _uncompressedSize;
            }
        }
        /// <summary>
        /// The ZipLibrary that this entry belongs to. If this entry has been deleted, this will return null.
        /// </summary>
        public ZipLibrary Library { get { return _Library; } }

        /// <summary>
        /// Opens the entry. If the Library that the entry belongs to was opened in Read mode, the returned stream will be readable, and it may or may not be seekable. If Create mode, the returned stream will be writeable and not seekable. If Update mode, the returned stream will be readable, writeable, seekable, and support SetLength.
        /// </summary>
        /// <returns>A Stream that represents the contents of the entry.</returns>
        /// <exception cref="IOException">The entry is already currently open for writing. -or- The entry has been deleted from the Library. -or- The Library that this entry belongs to was opened in ZipLibraryMode.Create, and this entry has already been written to once.</exception>
        /// <exception cref="InvalidDataException">The entry is missing from the Library or is corrupt and cannot be read. -or- The entry has been compressed using a compression method that is not supported.</exception>
        /// <exception cref="ObjectDisposedException">The ZipLibrary that this entry belongs to has been disposed.</exception>
        public Stream Open()
        {
            Contract.Ensures(Contract.Result<Stream>() != null);

            ThrowIfInvalidLibrary();

            switch (_Library.Mode)
            {
                case ZipLibraryMode.Read:
                    return OpenInReadMode(true);
                case ZipLibraryMode.Create:
                    return OpenInWriteMode();
                case ZipLibraryMode.Update:
                default:
                    Contract.Assert(_Library.Mode == ZipLibraryMode.Update);
                    return OpenInUpdateMode();
            }
        }

        /// <summary>
        /// Deletes the entry from the Library.
        /// </summary>
        /// <exception cref="IOException">The entry is already open for reading or writing.</exception>
        /// <exception cref="NotSupportedException">The ZipLibrary that this entry belongs to was opened in a mode other than ZipLibraryMode.Update. </exception>
        /// <exception cref="ObjectDisposedException">The ZipLibrary that this entry belongs to has been disposed.</exception>
        public void Delete()
        {
            if (_Library == null)
                return;
            _Library.ThrowIfDisposed();

            if (_currentlyOpenForWrite)
                throw new IOException(Mobile.Utility.Lib.IO.Compression.Messages.DeleteOpenEntry);

            if (_Library.Mode != ZipLibraryMode.Update)
                throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.DeleteOnlyInUpdate);

            _Library.RemoveEntry(this);
            _Library = null;
            UnloadStreams();
        }

        /// <summary>
        /// Returns the FullName of the entry.
        /// </summary>
        /// <returns>FullName of the entry</returns>
        public override String ToString()
        {
            Contract.Ensures(Contract.Result<String>() != null);

            return FullName;
        }

        /*
        public void MoveTo(String destinationEntryName)
        {
            if (destinationEntryName == null)
                throw new ArgumentNullException("destinationEntryName");

            if (String.IsNullOrEmpty(destinationEntryName))
                throw new ArgumentException("destinationEntryName cannot be empty", "destinationEntryName");

            if (_Library == null)
                throw new InvalidOperationException("Attempt to move a deleted entry");

            if (_Library._isDisposed)
                throw new ObjectDisposedException(_Library.ToString());

            if (_Library.Mode != ZipLibraryMode.Update)
                throw new NotSupportedException("MoveTo can only be used when the Library is in Update mode");

            String oldFilename = _filename;
            _filename = destinationEntryName;
            if (_filenameLength > UInt16.MaxValue)
            {
                _filename = oldFilename;
                throw new ArgumentException("Library entry names must be smaller than 2^16 bytes");
            }
        }
        */

        /// <summary>
        /// Creates a file on the file system with the entry’s contents and the specified name. The last write time of the file is set to the entry’s last write time. This method does allows overwriting of an existing file with the same name.
        /// </summary>
        /// <exception cref="UnauthorizedAccessException">The caller does not have the required permission.</exception>
        /// <exception cref="ArgumentException">destinationFileName is a zero-length string, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars. -or- destinationFileName specifies a directory.</exception>
        /// <exception cref="ArgumentNullException">destinationFileName is null.</exception>
        /// <exception cref="PathTooLongException">The specified path, file name, or both exceed the system-defined maximum length. For example, on Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 characters.</exception>
        /// <exception cref="DirectoryNotFoundException">The path specified in destinationFileName is invalid (for example, it is on an unmapped drive).</exception>
        /// <exception cref="IOException">destinationFileName exists and overwrite is false. -or- An I/O error has occurred. -or- The entry is currently open for writing. -or- The entry has been deleted from the Library.</exception>
        /// <exception cref="NotSupportedException">destinationFileName is in an invalid format -or- The ZipLibrary that this entry belongs to was opened in a write-only mode.</exception>
        /// <exception cref="InvalidDataException">The entry is missing from the Library or is corrupt and cannot be read -or- The entry has been compressed using a compression method that is not supported.</exception>
        /// <exception cref="ObjectDisposedException">The ZipLibrary that this entry belongs to has been disposed.</exception>
        /// <param name="destinationFileName">The name of the file that will hold the contents of the entry. The path is permitted to specify relative or absolute path information. Relative path information is interpreted as relative to the current working directory.</param>
        /// <param name="overwrite">True to indicate overwrite.</param>
        public void ExtractToFile(String destinationFileName, Boolean overwrite)
        {
            //relies on File.Open for checking destinationFileName parameter

            using (Stream fs = File.Open(destinationFileName, overwrite ? FileMode.Create : FileMode.CreateNew))
            using (Stream es = Open())
            {
                es.CopyTo(fs);
            }
            File.SetLastWriteTime(destinationFileName, LastWriteTime.DateTime);
        }

        /// <summary>
        /// Creates a file on the file system with the entry’s contents and the specified name. The last write time of the file is set to the entry’s last write time. This method does not allow overwriting of an existing file with the same name. Attempting to extract explicit directories (entries with names that end in directory separator characters) will not result in the creation of a directory.
        /// </summary>
        /// <exception cref="UnauthorizedAccessException">The caller does not have the required permission.</exception>
        /// <exception cref="ArgumentException">destinationFileName is a zero-length string, contains only white space, or contains one or more invalid characters as defined by InvalidPathChars. -or- destinationFileName specifies a directory.</exception>
        /// <exception cref="ArgumentNullException">destinationFileName is null.</exception>
        /// <exception cref="PathTooLongException">The specified path, file name, or both exceed the system-defined maximum length. For example, on Windows-based platforms, paths must be less than 248 characters, and file names must be less than 260 characters.</exception>
        /// <exception cref="DirectoryNotFoundException">The path specified in destinationFileName is invalid (for example, it is on an unmapped drive).</exception>
        /// <exception cref="IOException">destinationFileName exists. -or- An I/O error has occurred. -or- The entry is currently open for writing. -or- The entry has been deleted from the Library.</exception>
        /// <exception cref="NotSupportedException">destinationFileName is in an invalid format -or- The ZipLibrary that this entry belongs to was opened in a write-only mode.</exception>
        /// <exception cref="InvalidDataException">The entry is missing from the Library or is corrupt and cannot be read -or- The entry has been compressed using a compression method that is not supported.</exception>
        /// <exception cref="ObjectDisposedException">The ZipLibrary that this entry belongs to has been disposed.</exception>
        /// <param name="destinationFileName">The name of the file that will hold the contents of the entry. The path is permitted to specify relative or absolute path information. Relative path information is interpreted as relative to the current working directory.</param>
        public void ExtractToFile(String destinationFileName)
        {
            ExtractToFile(destinationFileName, false);
        }

        #region private properties

        private Int64 OffsetOfCompressedData
        {
            get
            {
                if (_storedOffsetOfCompressedData == null)
                {
                    _Library.LibraryStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin);
                    //by calling this, we are using local header filenameLength and extraFieldLength
                    //to find start of data, but still using central directory size information
                    if (!ZipLocalFileHeader.TrySkipBlock(_Library.LibraryReader))
                         throw new InvalidDataException(Mobile.Utility.Lib.IO.Compression.Messages.LocalFileHeaderCorrupt);
                    _storedOffsetOfCompressedData = _Library.LibraryStream.Position;
                }
                return _storedOffsetOfCompressedData.Value;
            }
        }
        private MemoryStream UncompressedData
        {
            get
            {
                if (_storedUncompressedData == null)
                {
                    //this means we have never opened it before

                    //if _uncompressedSize > Int32.MaxValue, it's still okay, because MemoryStream will just
                    //grow as data is copied into it
                    _storedUncompressedData = new MemoryStream((Int32)_uncompressedSize);

                    if (_originallyInLibrary)
                    {
                        using (Stream decompressor = OpenInReadMode(false))
                        {
                            try
                            {
                                decompressor.CopyTo(_storedUncompressedData);
                            }
                            catch (InvalidDataException)
                            {
                                /* this is the case where the Library say the entry is deflate, but deflateStream
                                 * throws an InvalidDataException. This property should only be getting accessed in
                                 * Update mode, so we want to make sure _storedUncompressedData stays null so
                                 * that later when we dispose the Library, this entry loads the compressedBytes, and
                                 * copies them straight over
                                 */
                                _storedUncompressedData.Dispose();
                                _storedUncompressedData = null;
                                _currentlyOpenForWrite = false;
                                _everOpenedForWrite = false;
                                throw;
                            }
                        }
                    }

                    //if they start modifying it, we should make sure it will get deflated
                    CompressionMethod = CompressionMethodValues.Deflate;
                }

                return _storedUncompressedData;
            }
        }
        private CompressionMethodValues CompressionMethod
        {
            get { return _storedCompressionMethod; }
            set
            {
                if (value == CompressionMethodValues.Deflate)
                    VersionToExtractAtLeast(ZipVersionNeededValues.Deflate);
                _storedCompressionMethod = value;
            }
        }
        private Encoding FilenameEncoding
        {
            get
            {
                if ((_generalPurposeBitFlag & BitFlagValues.UnicodeFileName) == 0)
                    return Encoding.GetEncoding(0);
                else
                    return Encoding.UTF8;
            }
        }
        private Int32 FilenameLength
        {
            get
            {
                //the only way _storedFilenameBytes would be null is if we try
                //to access FilenameLength before setting filename, but we do that in the contructors
                Contract.Assert(_storedFilenameBytes != null);

                return _storedFilenameBytes.Length;
            }
        }

        internal Boolean EverOpenedForWrite { get { return _everOpenedForWrite; } }

 
        #endregion

        #region private methods

        private CheckSumAndSizeWriteStream GetDataCompressor(Stream backingStream, Boolean leaveBackingStreamOpen, EventHandler onClose)
        {
            //stream stack: backingStream -> DeflateStream -> CheckSumWriteStream

            //we should always be compressing with deflate. Stored is used for empty files, but we don't actually
            //call through this function for that - we just write the stored value in the header
            Contract.Assert(CompressionMethod == CompressionMethodValues.Deflate);

            Stream compressorStream = new DeflateStream(backingStream, CompressionMode.Compress, leaveBackingStreamOpen);
            Boolean isIntermediateStream = true;

            Boolean leaveCompressorStreamOpenOnClose = leaveBackingStreamOpen && !isIntermediateStream;
            CheckSumAndSizeWriteStream checkSumStream =
                new CheckSumAndSizeWriteStream(compressorStream, backingStream, leaveCompressorStreamOpenOnClose,
                    (Int64 initialPosition, Int64 currentPosition, UInt32 checkSum) =>
                    {
                        _crc32 = checkSum;
                        _uncompressedSize = currentPosition;
                        _compressedSize = backingStream.Position - initialPosition;

                        if (onClose != null)
                            onClose(this, EventArgs.Empty);
                    });

            return checkSumStream;
        }

        private Stream GetDataDecompressor(Stream compressedStreamToRead)
        {
            Stream uncompressedStream = null;
            switch (CompressionMethod)
            {
                case CompressionMethodValues.Deflate:
                    uncompressedStream = new DeflateStream(compressedStreamToRead, CompressionMode.Decompress);
                    break;
                case CompressionMethodValues.Stored:
                default:
                    //we can assume that only deflate/stored are allowed because we assume that
                    //IsOpenable is checked before this function is called
                    Contract.Assert(CompressionMethod == CompressionMethodValues.Stored);

                    uncompressedStream = compressedStreamToRead;
                    break;
            }

            return uncompressedStream;
        }

        private Stream OpenInReadMode(Boolean checkOpenable)
        {
            if (checkOpenable)
                ThrowIfNotOpenable(true, false);

            Stream compressedStream = new SubReadStream(_Library.LibraryStream, OffsetOfCompressedData, _compressedSize);
            return GetDataDecompressor(compressedStream);
        }

        private Stream OpenInWriteMode()
        {
            if (_everOpenedForWrite)
                throw new IOException(Mobile.Utility.Lib.IO.Compression.Messages.CreateModeWriteOnceAndOneEntryAtATime);

            //we assume that if another entry grabbed the Library stream, that it set this entry's _everOpenedForWrite property to true by calling WriteLocalFileHeaderIfNeeed
            Contract.Assert(_Library.IsStillLibraryStreamOwner(this));

            _everOpenedForWrite = true;
            CheckSumAndSizeWriteStream crcSizeStream = GetDataCompressor(_Library.LibraryStream, true,
                                                            (Object o, EventArgs e) =>
                                                            {
                                                                //release the Library stream
                                                                _Library.ReleaseLibraryStream(this);
                                                                _outstandingWriteStream = null;
                                                            });
            _outstandingWriteStream = new DirectToLibraryWriterStream(crcSizeStream, this);

            return new WrappedStream(_outstandingWriteStream, (Object o, EventArgs e) => _outstandingWriteStream.Close());
        }

        private Stream OpenInUpdateMode()
        {
            if (_currentlyOpenForWrite)
                throw new IOException(Mobile.Utility.Lib.IO.Compression.Messages.UpdateModeOneStream);

            ThrowIfNotOpenable(true, true);

            _everOpenedForWrite = true;
            _currentlyOpenForWrite = true;
            //always put it at the beginning for them
            UncompressedData.Seek(0, SeekOrigin.Begin);
            return new WrappedStream(UncompressedData,
                                     (Object o, EventArgs e) =>
                                     {
                                         //once they close, we know uncompressed length, but still not compressed length
                                         //so we don't fill in any size information
                                         //those fields get figured out when we call GetCompressor as we write it to
                                         //the actual Library
                                         _currentlyOpenForWrite = false;
                                     });
        }

        private Boolean IsOpenable(Boolean needToUncompress, Boolean needToLoadIntoMemory, out String message)
        {
            message = null;

            if (_originallyInLibrary)
            {
                if (needToUncompress)
                {
                    if (CompressionMethod != CompressionMethodValues.Stored &&
                        CompressionMethod != CompressionMethodValues.Deflate)
                    {
                        message = Mobile.Utility.Lib.IO.Compression.Messages.UnsupportedCompression;
                        return false;
                    }
                }
                if (_diskNumberStart != _Library.NumberOfThisDisk)
                {
                    message = Mobile.Utility.Lib.IO.Compression.Messages.SplitSpanned;
                    return false;
                }
                if (_offsetOfLocalHeader > _Library.LibraryStream.Length)
                {
                    message = Mobile.Utility.Lib.IO.Compression.Messages.LocalFileHeaderCorrupt;
                    return false;
                }
                _Library.LibraryStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin);
                if (!ZipLocalFileHeader.TrySkipBlock(_Library.LibraryReader))
                {
                    message = Mobile.Utility.Lib.IO.Compression.Messages.LocalFileHeaderCorrupt;
                    return false;
                }
                //when this property gets called, some duplicated work
                if (OffsetOfCompressedData + _compressedSize > _Library.LibraryStream.Length)
                {
                    message = Mobile.Utility.Lib.IO.Compression.Messages.LocalFileHeaderCorrupt;
                    return false;
                }
                //this limitation exists because a) it is unreasonable to load > 4GB into memory
                //but also because the stream reading functions make it hard
                if (needToLoadIntoMemory)
                {
                    if (_compressedSize > Int32.MaxValue)
                    {
                        message = Mobile.Utility.Lib.IO.Compression.Messages.EntryTooLarge;
                        return false;
                    }
                }
            }

            return true;
        }

        private Boolean SizesTooLarge()
        {
            return _compressedSize > UInt32.MaxValue || _uncompressedSize > UInt32.MaxValue;
        }

        //return value is true if we allocated an extra field for 64 bit headers, un/compressed size
        private Boolean WriteLocalFileHeader(Boolean isEmptyFile)
        {
            BinaryWriter writer = new BinaryWriter(_Library.LibraryStream);

            //_filename only gets set when we read in or call moveTo. MoveTo does a check, and
            //reading in should not be able to produce a filename longer than UInt16.MaxValue
            Contract.Assert(FilenameLength <= UInt16.MaxValue);

            //decide if we need the Zip64 extra field:
            Zip64ExtraField zip64ExtraField = new Zip64ExtraField();
            Boolean zip64Used = false;
            UInt32 compressedSizeTruncated, uncompressedSizeTruncated;

            //if we already know that we have an empty file don't worry about anything, just do a straight shot of the header
            if (isEmptyFile)
            {
                CompressionMethod = CompressionMethodValues.Stored;
                compressedSizeTruncated = 0;
                uncompressedSizeTruncated = 0;
                Contract.Assert(_compressedSize == 0);
                Contract.Assert(_uncompressedSize == 0);
                Contract.Assert(_crc32 == 0);
            }
            else
            {
                //if we have a non-seekable stream, don't worry about sizes at all, and just set the right bit
                //if we are using the data descriptor, then sizes and crc should be set to 0 in the header
                if (_Library.Mode == ZipLibraryMode.Create && _Library.LibraryStream.CanSeek == false && !isEmptyFile)
                {
                    _generalPurposeBitFlag |= BitFlagValues.DataDescriptor;
                    zip64Used = false;
                    compressedSizeTruncated = 0;
                    uncompressedSizeTruncated = 0;
                    //the crc should not have been set if we are in create mode, but clear it just to be sure
                    Contract.Assert(_crc32 == 0);
                }
                else //if we are not in streaming mode, we have to decide if we want to write zip64 headers
                {

                    if (SizesTooLarge()
#if DEBUG_FORCE_ZIP64
 || (_Library._forceZip64 && _Library.Mode == ZipLibraryMode.Update)
#endif
                    )
                    {
                        zip64Used = true;
                        compressedSizeTruncated = ZipHelper.Mask32Bit;
                        uncompressedSizeTruncated = ZipHelper.Mask32Bit;

                        //prepare Zip64 extra field object. If we have one of the sizes, the other must go in there
                        zip64ExtraField.CompressedSize = _compressedSize;
                        zip64ExtraField.UncompressedSize = _uncompressedSize;

                        VersionToExtractAtLeast(ZipVersionNeededValues.Zip64);
                    }
                    else
                    {
                        zip64Used = false;
                        compressedSizeTruncated = (UInt32)_compressedSize;
                        uncompressedSizeTruncated = (UInt32)_uncompressedSize;
                    }
                }
            }

            //save offset
            _offsetOfLocalHeader = (UInt32)writer.BaseStream.Position;

            //calculate extra field. if zip64 stuff + original extraField aren't going to fit, dump the original extraField, because this is more important
            Int32 bigExtraFieldLength = (zip64Used ? zip64ExtraField.TotalSize : 0)
                                      + (_lhUnknownExtraFields != null ? ZipGenericExtraField.TotalSize(_lhUnknownExtraFields) : 0);
            UInt16 extraFieldLength;
            if (bigExtraFieldLength > UInt16.MaxValue)
            {
                extraFieldLength = (UInt16)(zip64Used ? zip64ExtraField.TotalSize : 0);
                _lhUnknownExtraFields = null;
            }
            else
            {
                extraFieldLength = (UInt16)bigExtraFieldLength;
            }

            //write header
            writer.Write(ZipLocalFileHeader.SignatureConstant);
            writer.Write((UInt16)_versionToExtract);
            writer.Write((UInt16)_generalPurposeBitFlag);
            writer.Write((UInt16)CompressionMethod);
            writer.Write(ZipHelper.DateTimeToDosTime(_lastModified.DateTime)); //UInt32
            writer.Write(_crc32);               //UInt32
            writer.Write(compressedSizeTruncated);  //UInt32
            writer.Write(uncompressedSizeTruncated); //UInt32
            writer.Write((UInt16)FilenameLength);
            writer.Write(extraFieldLength);    //UInt16

            writer.Write(FilenameEncoding.GetBytes(FullName));

            if (zip64Used)
                zip64ExtraField.WriteBlock(_Library.LibraryStream);
            if (_lhUnknownExtraFields != null)
                ZipGenericExtraField.WriteAllBlocks(_lhUnknownExtraFields, _Library.LibraryStream);

            return zip64Used;
        }

        private void WriteLocalFileHeaderAndDataIfNeeded()
        {
            //_storedUncompressedData gets frozen here, and is what gets written to the file
            if (_storedUncompressedData != null || _compressedBytes != null)
            {

                if (_storedUncompressedData != null)
                {
                    _uncompressedSize = _storedUncompressedData.Length;

                    //The compressor fills in CRC and sizes
                    //The DirectToLibraryWriterStream writes headers and such
                    using (Stream entryWriter = new DirectToLibraryWriterStream(
                                                    GetDataCompressor(_Library.LibraryStream, true, null),
                                                    this))
                    {
                        _storedUncompressedData.Seek(0, SeekOrigin.Begin);
                        _storedUncompressedData.CopyTo(entryWriter);
                        _storedUncompressedData.Close();
                        _storedUncompressedData = null;
                    }
                }
                else
                {
                    // we know the sizes at this point, so just go ahead and write the headers
                    if (_uncompressedSize == 0)
                        CompressionMethod = CompressionMethodValues.Stored;
                    WriteLocalFileHeader(false);
                    // we just want to copy the compressed bytes straight over, but the stream apis
                    // don't handle larger than 32 bit values, so we just wrap it in a stream
                    using (MemoryStream compressedStream = new MemoryStream(_compressedBytes))
                    {
                        compressedStream.CopyTo(_Library.LibraryStream);
                    }
                }
            }
            else //there is no data in the file, but if we are in update mode, we still need to write a header
            {
                if (_Library.Mode == ZipLibraryMode.Update || !_everOpenedForWrite)
                {
                    _everOpenedForWrite = true;
                    WriteLocalFileHeader(true);
                }
            }
        }

        /* Using _offsetOfLocalHeader, seeks back to where CRC and sizes should be in the header,
         * writes them, then seeks back to where you started
         * Assumes that the stream is currently at the end of the data
        */
        private void WriteCrcAndSizesInLocalHeader(Boolean zip64HeaderUsed)
        {
            Int64 finalPosition = _Library.LibraryStream.Position;
            BinaryWriter writer = new BinaryWriter(_Library.LibraryStream);

            Boolean zip64Needed = SizesTooLarge()
#if DEBUG_FORCE_ZIP64
 || _Library._forceZip64
#endif
;
            Boolean pretendStreaming = zip64Needed && !zip64HeaderUsed;

            UInt32 compressedSizeTruncated = zip64Needed ? ZipHelper.Mask32Bit : (UInt32)_compressedSize;
            UInt32 uncompressedSizeTruncated = zip64Needed ? ZipHelper.Mask32Bit : (UInt32)_uncompressedSize;

            /* first step is, if we need zip64, but didn't allocate it, pretend we did a stream write, because
             * we can't go back and give ourselves the space that the extra field needs.
             * we do this by setting the correct property in the bit flag */
            if (pretendStreaming)
            {
                _generalPurposeBitFlag |= BitFlagValues.DataDescriptor;

                _Library.LibraryStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.OffsetToBitFlagFromHeaderStart,
                                            SeekOrigin.Begin);
                writer.Write((UInt16)_generalPurposeBitFlag);
            }

            /* next step is fill out the 32-bit size values in the normal header. we can't assume that
             * they are correct. we also write the CRC */
            _Library.LibraryStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.OffsetToCrcFromHeaderStart,
                                            SeekOrigin.Begin);
            if (!pretendStreaming)
            {
                writer.Write(_crc32);
                writer.Write(compressedSizeTruncated);
                writer.Write(uncompressedSizeTruncated);
            }
            else //but if we are pretending to stream, we want to fill in with zeroes
            {
                writer.Write((UInt32)0);
                writer.Write((UInt32)0);
                writer.Write((UInt32)0);
            }

            /* next step: if we wrote the 64 bit header initially, a different implementation might
             * try to read it, even if the 32-bit size values aren't masked. thus, we should always put the
             * correct size information in there. note that order of uncomp/comp is switched, and these are
             * 64-bit values
             * also, note that in order for this to be correct, we have to insure that the zip64 extra field
             * is alwasy the first extra field that is written */
            if (zip64HeaderUsed)
            {
                _Library.LibraryStream.Seek(_offsetOfLocalHeader + ZipLocalFileHeader.SizeOfLocalHeader
                                            + FilenameLength + Zip64ExtraField.OffsetToFirstField,
                                            SeekOrigin.Begin);
                writer.Write(_uncompressedSize);
                writer.Write(_compressedSize);

                _Library.LibraryStream.Seek(finalPosition, SeekOrigin.Begin);
            }

            // now go to the where we were. assume that this is the end of the data
            _Library.LibraryStream.Seek(finalPosition, SeekOrigin.Begin);

            /* if we are pretending we did a stream write, we want to write the data descriptor out
             * the data descriptor can have 32-bit sizes or 64-bit sizes. In this case, we always use
             * 64-bit sizes */
            if (pretendStreaming)
            {
                writer.Write(_crc32);
                writer.Write(_compressedSize);
                writer.Write(_uncompressedSize);
            }
        }

        private void WriteDataDescriptor()
        {
            // data descriptor can be 32-bit or 64-bit sizes. 32-bit is more compatible, so use that if possible
            // signature is optional but recommended by the spec

            BinaryWriter writer = new BinaryWriter(_Library.LibraryStream);

            writer.Write(ZipLocalFileHeader.DataDescriptorSignature);
            writer.Write(_crc32);
            if (SizesTooLarge())
            {
                writer.Write(_compressedSize);
                writer.Write(_uncompressedSize);
            }
            else
            {
                writer.Write((UInt32)_compressedSize);
                writer.Write((UInt32)_uncompressedSize);
            }
        }

        private void UnloadStreams()
        {
            if (_storedUncompressedData != null)
                _storedUncompressedData.Close();
            _compressedBytes = null;
            _outstandingWriteStream = null;
        }

        private void CloseStreams()
        {
            //if the user left the stream open, close the underlying stream for them
            if (_outstandingWriteStream != null)
            {
                _outstandingWriteStream.Close();
            }
        }

        private void VersionToExtractAtLeast(ZipVersionNeededValues value)
        {
            if (_versionToExtract < value)
            {
                _versionToExtract = value;
            }
        }

        private void ThrowIfInvalidLibrary()
        {
            if (_Library == null)
                throw new InvalidOperationException(Mobile.Utility.Lib.IO.Compression.Messages.DeletedEntry);
            _Library.ThrowIfDisposed();
        }

        #endregion

        #region internal methods

        /* does almost everything you need to do to forget about this entry
         * writes the local header/data, gets rid of all the data,
         * closes all of the streams except for the very outermost one that
         * the user holds on to and is responsible for closing
         * 
         * after calling this, and only after calling this can we be guaranteed
         * that we are reading to write the central directory
         * 
         * should only throw an exception in extremely exceptional cases because it is called from dispose
         */
        internal void WriteAndFinishLocalEntry()
        {
            CloseStreams();
            WriteLocalFileHeaderAndDataIfNeeded();
            UnloadStreams();
        }

        //should only throw an exception in extremely exceptional cases because it is called from dispose
        internal void WriteCentralDirectoryFileHeader()
        {
            //This part is simple, because we should definitely know the sizes by this time
            BinaryWriter writer = new BinaryWriter(_Library.LibraryStream);

            //_filename only gets set when we read in or call moveTo. MoveTo does a check, and
            //reading in should not be able to produce a filename longer than UInt16.MaxValue
            Contract.Assert(FilenameLength <= UInt16.MaxValue);

            //decide if we need the Zip64 extra field:
            Zip64ExtraField zip64ExtraField = new Zip64ExtraField();
            UInt32 compressedSizeTruncated, uncompressedSizeTruncated, offsetOfLocalHeaderTruncated;

            Boolean zip64Needed = false;

            if (SizesTooLarge()
#if DEBUG_FORCE_ZIP64
 || _Library._forceZip64
#endif
)
            {
                zip64Needed = true;
                compressedSizeTruncated = ZipHelper.Mask32Bit;
                uncompressedSizeTruncated = ZipHelper.Mask32Bit;

                //If we have one of the sizes, the other must go in there as speced for LH, but not necessarily for CH, but we do it anyways
                zip64ExtraField.CompressedSize = _compressedSize;
                zip64ExtraField.UncompressedSize = _uncompressedSize;
            }
            else
            {
                compressedSizeTruncated = (UInt32)_compressedSize;
                uncompressedSizeTruncated = (UInt32)_uncompressedSize;
            }


            if (_offsetOfLocalHeader > UInt32.MaxValue
#if DEBUG_FORCE_ZIP64
 || _Library._forceZip64
#endif
)
            {
                zip64Needed = true;
                offsetOfLocalHeaderTruncated = ZipHelper.Mask32Bit;

                //If we have one of the sizes, the other must go in there as speced for LH, but not necessarily for CH, but we do it anyways
                zip64ExtraField.LocalHeaderOffset = _offsetOfLocalHeader;
            }
            else
            {
                offsetOfLocalHeaderTruncated = (UInt32)_offsetOfLocalHeader;
            }

            if (zip64Needed)
                VersionToExtractAtLeast(ZipVersionNeededValues.Zip64);

            //determine if we can fit zip64 extra field and original extra fields all in
            Int32 bigExtraFieldLength = (zip64Needed ? zip64ExtraField.TotalSize : 0)
                                      + (_cdUnknownExtraFields != null ? ZipGenericExtraField.TotalSize(_cdUnknownExtraFields) : 0);
            UInt16 extraFieldLength;
            if (bigExtraFieldLength > UInt16.MaxValue)
            {
                extraFieldLength = (UInt16)(zip64Needed ? zip64ExtraField.TotalSize : 0);
                _cdUnknownExtraFields = null;
            }
            else
            {
                extraFieldLength = (UInt16)bigExtraFieldLength;
            }

            writer.Write(ZipCentralDirectoryFileHeader.SignatureConstant);
            writer.Write((UInt16)_versionToExtract);
            writer.Write((UInt16)_versionToExtract);    //this is the version made by field. low byte is version needed, and high byte is 0 for MS DOS
            writer.Write((UInt16)_generalPurposeBitFlag);
            writer.Write((UInt16)CompressionMethod);
            writer.Write(ZipHelper.DateTimeToDosTime(_lastModified.DateTime)); //UInt32
            writer.Write(_crc32);   //UInt32
            writer.Write(compressedSizeTruncated);  //UInt32
            writer.Write(uncompressedSizeTruncated);    //UInt32
            writer.Write((UInt16)FilenameLength);
            writer.Write(extraFieldLength);    //UInt16

            if (_fileComment != null)
                Contract.Assert(_fileComment.Length <= UInt16.MaxValue); // this should hold because of how we read it originally in ZipCentralDirectoryFileHeader

            writer.Write(_fileComment != null ? (UInt16)_fileComment.Length : (UInt16)0);    //file comment length
            writer.Write((UInt16)0);    //disk number start
            writer.Write((UInt16)0);    //internal file attributes
            writer.Write((UInt32)0);    //external file attributes
            writer.Write(offsetOfLocalHeaderTruncated); //offset of local header

            writer.Write(FilenameEncoding.GetBytes(FullName));

            //write extra fields
            if (zip64Needed)
                zip64ExtraField.WriteBlock(_Library.LibraryStream);
            if (_cdUnknownExtraFields != null)
                ZipGenericExtraField.WriteAllBlocks(_cdUnknownExtraFields, _Library.LibraryStream);

            if (_fileComment != null)
                writer.Write(_fileComment);
        }

        //returns false if fails, will get called on every entry before closing in update mode
        //can throw InvalidDataException
        internal Boolean LoadLocalHeaderExtraFieldAndCompressedBytesIfNeeded()
        {
            String message;
            //we should have made this exact call in _Library.Init through ThrowIfOpenable
            Contract.Assert(IsOpenable(false, true, out message));

            //load local header's extra fields. it will be null if we couldn't read for some reason
            if (_originallyInLibrary)
            {
                _Library.LibraryStream.Seek(_offsetOfLocalHeader, SeekOrigin.Begin);

                _lhUnknownExtraFields = ZipLocalFileHeader.GetExtraFields(_Library.LibraryReader);
            }

            if (!_everOpenedForWrite && _originallyInLibrary)
            {
                //we know that it is openable at this point

                _compressedBytes = new Byte[_compressedSize];
                _Library.LibraryStream.Seek(OffsetOfCompressedData, SeekOrigin.Begin);
                //casting _compressedSize to Int32 here is safe because of check in IsOpenable
                ZipHelper.ReadBytes(_Library.LibraryStream, _compressedBytes, (Int32)_compressedSize);
            }

            return true;
        }

        internal void ThrowIfNotOpenable(Boolean needToUncompress, Boolean needToLoadIntoMemory)
        {
            String message;
            if (!IsOpenable(needToUncompress, needToLoadIntoMemory, out message))
                throw new InvalidDataException(message);
        }

        #endregion

        #region nested classes/ enums

        private class DirectToLibraryWriterStream : Stream
        {
            #region fields

            private Int64 _position;
            private CheckSumAndSizeWriteStream _crcSizeStream;
            private Boolean _everWritten;
            private Boolean _isDisposed;
            private ZipLibraryEntry _entry;
            private Boolean _usedZip64inLH;
            private Boolean _canWrite;

            #endregion

            #region constructors

            //makes the assumption that somewhere down the line, crcSizeStream is eventually writing directly to the Library
            //this class calls other functions on ZipLibraryEntry that write directly to the Library
            public DirectToLibraryWriterStream(CheckSumAndSizeWriteStream crcSizeStream, ZipLibraryEntry entry)
            {
                _position = 0;
                _crcSizeStream = crcSizeStream;
                _everWritten = false;
                _isDisposed = false;
                _entry = entry;
                _usedZip64inLH = false;
                _canWrite = true;
            }

            #endregion

            #region properties

            public override Int64 Length
            {
                get
                {
                    ThrowIfDisposed();
                    throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.SeekingNotSupported);
                }
            }
            public override Int64 Position
            {
                get
                {
                    Contract.Ensures(Contract.Result<Int64>() >= 0);

                    ThrowIfDisposed();
                    return _position;
                }
                set
                {
                    ThrowIfDisposed();
                    throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.SeekingNotSupported);
                }
            }

            public override Boolean CanRead { get { return false; } }
            public override Boolean CanSeek { get { return false; } }
            public override Boolean CanWrite { get { return _canWrite; } }

            #endregion

            #region methods

            private void ThrowIfDisposed()
            {
                if (_isDisposed)
                    throw new ObjectDisposedException(this.GetType().Name, Mobile.Utility.Lib.IO.Compression.Messages.HiddenStreamName);
            }

            public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
            {
                ThrowIfDisposed();
                throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.ReadingNotSupported);
            }

            public override Int64 Seek(Int64 offset, SeekOrigin origin)
            {
                ThrowIfDisposed();
                throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.SeekingNotSupported);
            }

            public override void SetLength(Int64 value)
            {
                ThrowIfDisposed();
                throw new NotSupportedException(Mobile.Utility.Lib.IO.Compression.Messages.SetLengthRequiresSeekingAndWriting);
            }

            //careful: assumes that write is the only way to write to the stream, if writebyte/beginwrite are implemented
            //they must set _everWritten, etc.
            public override void Write(Byte[] buffer, Int32 offset, Int32 count)
            {
                //we can't pass the argument checking down a level
                if (buffer == null)
                    throw new ArgumentNullException("buffer");
                if (offset < 0)
                    throw new ArgumentOutOfRangeException("offset", Mobile.Utility.Lib.IO.Compression.Messages.ArgumentNeedNonNegative);
                if (count < 0)
                    throw new ArgumentOutOfRangeException("count", Mobile.Utility.Lib.IO.Compression.Messages.ArgumentNeedNonNegative);
                if ((buffer.Length - offset) < count)
                    throw new ArgumentException(Mobile.Utility.Lib.IO.Compression.Messages.OffsetLengthInvalid);
                Contract.EndContractBlock();

                ThrowIfDisposed();
                Contract.Assert(CanWrite);

                //if we're not actually writing anything, we don't want to trigger the header
                if (count == 0)
                    return;

                if (!_everWritten)
                {
                    _everWritten = true;
                    //write local header, we are good to go
                    _usedZip64inLH = _entry.WriteLocalFileHeader(false);
                }

                _crcSizeStream.Write(buffer, offset, count);
                _position += count;
            }

            public override void Flush()
            {
                ThrowIfDisposed();
                Contract.Assert(CanWrite);
                
                _crcSizeStream.Flush();
            }

            protected override void Dispose(Boolean disposing)
            {
                if (disposing && !_isDisposed)
                {
                    _crcSizeStream.Close(); //now we have size/crc info

                    if (!_everWritten)
                    {
                        //write local header, no data, so we use stored
                        _entry.WriteLocalFileHeader(true);
                    }
                    else
                    {
                        //go back and finish writing
                        if (_entry._Library.LibraryStream.CanSeek)
                            //finish writing local header if we have seek capabilities

                            _entry.WriteCrcAndSizesInLocalHeader(_usedZip64inLH);
                        else
                            //write out data descriptor if we don't have seek capabilities
                            _entry.WriteDataDescriptor();
                    }
                    _canWrite = false;
                    _isDisposed = true;
                }

                base.Dispose(disposing);
            }

            #endregion
        }

        [Flags]
        private enum BitFlagValues : ushort { DataDescriptor = 0x8, UnicodeFileName = 0x800 }

        private enum CompressionMethodValues : ushort { Stored = 0x0, Deflate = 0x8 }

        private enum OpenableValues { Openable, FileNonExistent, FileTooLarge }

        #endregion

        #region fields

        private const UInt16 DefaultVersionToExtract = 10;

        private ZipLibrary _Library;
        private readonly Boolean _originallyInLibrary;
        private readonly Int32 _diskNumberStart;
        private ZipVersionNeededValues _versionToExtract;
        //Also, always use same for version made by because MSDOS high byte is 0
        private BitFlagValues _generalPurposeBitFlag;
        private CompressionMethodValues _storedCompressionMethod;
        private DateTimeOffset _lastModified;
        private Int64 _compressedSize;
        private Int64 _uncompressedSize;
        private Int64 _offsetOfLocalHeader;
        private Int64? _storedOffsetOfCompressedData;
        private UInt32 _crc32;
        private Byte[] _compressedBytes;
        private MemoryStream _storedUncompressedData;
        private Boolean _currentlyOpenForWrite;
        private Boolean _everOpenedForWrite;
        private Stream _outstandingWriteStream;
        private String _storedFilename;
        private Byte[] _storedFilenameBytes;
        //only apply to update mode
        private List<ZipGenericExtraField> _cdUnknownExtraFields;
        private List<ZipGenericExtraField> _lhUnknownExtraFields;
        private Byte[] _fileComment;

        #endregion

        #region constructors

        //Initializes, attaches it to Library
        internal ZipLibraryEntry(ZipLibrary Library, ZipCentralDirectoryFileHeader cd)
        {
            _Library = Library;

            _originallyInLibrary = true;

            _diskNumberStart = cd.DiskNumberStart;
            _versionToExtract = (ZipVersionNeededValues)cd.VersionNeededToExtract;
            _generalPurposeBitFlag = (BitFlagValues)cd.GeneralPurposeBitFlag;
            CompressionMethod = (CompressionMethodValues)cd.CompressionMethod;
            _lastModified = new DateTimeOffset(ZipHelper.DosTimeToDateTime(cd.LastModified));
            _compressedSize = cd.CompressedSize;
            _uncompressedSize = cd.UncompressedSize;
            _offsetOfLocalHeader = cd.RelativeOffsetOfLocalHeader;
            /* we don't know this yet: should be _offsetOfLocalHeader + 30 + filenamelength + extrafieldlength
                * but filename/extra length could be different in LH
                */
            _storedOffsetOfCompressedData = null;
            _crc32 = cd.Crc32;

            _compressedBytes = null;
            _storedUncompressedData = null;
            _currentlyOpenForWrite = false;
            _everOpenedForWrite = false;
            _outstandingWriteStream = null;

            FullName = FilenameEncoding.GetString(cd.Filename);

            _lhUnknownExtraFields = null;
            //the cd should have these as null if we aren't in Update mode
            _cdUnknownExtraFields = cd.ExtraFields;
            _fileComment = cd.FileComment;
        }

        //Initializes new entry
        internal ZipLibraryEntry(ZipLibrary Library, String entryName)
        {
            _Library = Library;

            _originallyInLibrary = false;

            _diskNumberStart = 0;

            _versionToExtract = ZipVersionNeededValues.Default; //this must happen before following two assignment
            _generalPurposeBitFlag = 0;
            CompressionMethod = CompressionMethodValues.Deflate;
            _lastModified = DateTimeOffset.Now;

            _compressedSize = 0; //we don't know these yet
            _uncompressedSize = 0;
            _offsetOfLocalHeader = 0;
            _storedOffsetOfCompressedData = null;
            _crc32 = 0;

            _compressedBytes = null;
            _storedUncompressedData = null;
            _currentlyOpenForWrite = false;
            _everOpenedForWrite = false;
            _outstandingWriteStream = null;

            FullName = entryName;

            _cdUnknownExtraFields = null;
            _lhUnknownExtraFields = null;
            _fileComment = null;

            if (FilenameLength > UInt16.MaxValue)
                throw new ArgumentException(Mobile.Utility.Lib.IO.Compression.Messages.EntryNamesTooLong);

            //grab the stream if we're in create mode
            if (_Library.Mode == ZipLibraryMode.Create)
            {
                _Library.AcquireLibraryStream(this);
            }
        }

        #endregion
    }
}